---

title: 互斥锁
description: 解决原子性问题
---
原子性问题的源头是**线程切换**，如果能够禁用线程切换那不就能解决这个问题了吗?操作系统做线程切换是依赖 CPU 中断的，所以禁止 CPU 发生中断就能够禁止线程切换。

在单核 CPU 上，这个方案是可行的，但是不适合多核 CPU 。

“**同一时刻只有一个线程执行**”称为互斥。如果能够保证对共享变量的修改是互斥的，那么无论是单核 CPU 还是多核 CPU ，就都能保证原子性了。

## 锁模型

![287008c8137a43fa032e68a0c23c172f](https://img.wkq.pub/pic/287008c8137a43fa032e68a0c23c172f.webp)

* 临界区： 一段需要互斥访问的代码，是访问受保护资源的路径。
* 受保护资源： 共享变量
* 保护资源 R 的锁： 在现实世界里，锁和锁要保护的资源是有对应关系的，在并发领域同样如此。
* 加锁和解锁： 进入临界区访问受保护资源时，就需要加锁（保证同一时刻只有一个线程执行）避免其它线程的访问；

## synchronized
锁 是一种通用的技术方案，Java 语言提供的 synchronized 关键字，就是锁的一种实现。

synchronized 关键字可以用来修饰方法，也可以用来修饰代码块。

```java
class X{
    //修饰非静态方法
    synchronized void foo(){
        //临界区
    }
    //修饰静态方法
    static synchronized void bar(){
        //临界区
    }
    //修饰代码块
    Object obj = new Object();
    void baz(){
        synchronized(obj){
            //临界区
        }
    }
}
```
Java 编译器会在 synchronized 修饰的方法或代码块前后自动加上加锁 lock() 和解锁 unlock（）。

修饰方法的时候锁定的是什么呢？这个也是 Java 的一条隐形规则：

> 当修饰静态方法的时候；锁定的是当前类的 Class 对象，在上面的例子中就是 Class X；当修饰非静态方法的时候，锁定的就是当前实例对象 this。

```java title='synchronized 修饰静态方法相当于:'
class X {
    //修饰静态方法
    synchronized(X.class) static void bar(){
        //临界区
    }
}
```
```java title='synchronized 修饰方法相当于：'
class X {
    //修饰非静态方法
    synchronized(this) void foo(){
        //临界区
    }
}
```
## 使用 synchronized 解决 count+1 问题
```java
class SafeCalc {
    long value = 0L;
    long get() {
        return value;
    }
    synchronized void addOne(){
        value += 1;
    }
}
```

对于 addOne 方法，被 synchronized 修饰后，无论是单核 CPU 还是多核 CPU ，只有一个线程能够执行 addOne() 方法，所以一定能保证原子性；根据管程中锁的规则，可见性和有序性也是可以保证的。

如果有 10000 个线程同时执行 addOne() 方法，最终结果一定是 10000。

对于 get() 方法。执行 addOne() 方法后，value 的值对 get() 方法是不可见的。将 get() 方法也用 synchronized  修饰，这样线程要进入临界区 get()  和 addOne() ,必须先获得 this 这把锁，这样 get()  和 addOne() 也是互斥的。

![26a84ffe2b4a6ae67c8093d29473e1f6](https://img.wkq.pub/pic/26a84ffe2b4a6ae67c8093d29473e1f6.webp)

## 锁和受保护资源的关系
**受保护资源和锁之间的关系是 N:1 的关系**

```java title='如果将 value 改成静态变量，把 addOne 改成静态方法‘

class SafeCalc {
  static long value = 0L;
  synchronized long get() {
    return value;
  }
  synchronized static void addOne() {
    value += 1;
  }
}
```

此时的代码是两个锁保护一个资源。这个受保护资源是静态变量 value，两个锁分别是 this 和 SafeCalc.class 。可以用下面这个图表示：

![60551e006fca96f581f3dc25424226be](https://img.wkq.pub/pic/60551e006fca96f581f3dc25424226be.webp)

由于临界区 get() 和 addOne() 是两个锁保护的，因此这两个临界区没有互斥关系，临界区 addOne() 对 value 的修改对临界区 get() 没有可见性保证，这就到导致了并发问题。

## 用一把锁保护多个资源

可以用一把锁来保护多个资源，但是不能用多把锁来保护一个资源。

当使用一把锁保护多个资源时，首先要区分这些资源是否存在关联关系。

### 保护没有关联关系的多个资源
假设账户类 Account 有两个成员变量，分别是账户余额 balance 和账户密码 password 。取款 withdraw() 和查看余额 getBalance() 操作会访问账户余额 balance，创建一个 final 对象 balLock 作为锁；
而更改密码 updatePassword() 和 查看密码 getPassword()  操作会修改账户密码 password，创建一个 final 对象 pwLock 作为锁。不同的资源使用不同的锁保护，各自管各自的。
```java
public class Account {
    /**
     * 锁：保护账户余额
     */
    private final Object balLock = new Object();
    /**
     * 账户余额
     */
    private Integer balance;
    /**
     * 锁： 保护账户密码
     */
    private final Object pwdLock = new Object();
    /**
     * 账户密码
     */
    private String password;

    /**
     * 取款
     */
    public void withdraw(Integer amt) {
        synchronized (balLock) {
            if (this.balance > amt) {
                this.balance -= amt;
            }
        }
    }
    /**
     * 查看余额
     */
    public Integer getBalance() {
        synchronized (balLock) {
            return balance;
        }
    }
    /**
     * 更改密码
     */
    public void updatePassword(String pwd) {
        this.password = pwd;
    }
    /**
     * 查看密码
     */
    public String getPassword() {
        synchronized (pwdLock) {
            return password;
        }
    }
}
```
当然，也可以使用一把互斥锁来保护多个资源（比如用 this 管理庄户类里的所有资源），但是用一把锁有一个问题，就是性能太差，会导致操作多个资源的操作都是串行的。而使用两把锁，取款和修改密码是可以并行的。

**用不同的锁对受保护的资源进行精细化管理，能提升性能。这种锁也叫做细粒度锁。**

### 保护有关联关系的多个资源
例如转账操作，账户 A 减少 100 元，账户 B 增加 100 元。这两个账户就是有关联关系的。

有一个 Account 类，该类有一个成员变量余额，还有一个用于转账的方法： transfer()，那么怎么保证 transfer() 的并发问题呢？

```java
public class Account {
    private int balance;

   synchronized void transfer(Account target, int amt) {
        if (this.balance > amt) {
            this.balance -= amt;
            target.balance += amt;
        }
    }
}
```
上述代码直接用 synchronized 修饰后，临界区内有两个资源 ，分别是转出账户的余额 this.balance 和传入账户的余额 target.balance,并且用的是同一把锁 this。

但 this 可以保护自己的余额 this.balance, 却保护不了别人的余额 target.balance 。

![1ba92a09d1a55a6a1636318f30c155d8](https://img.wkq.pub/pic/1ba92a09d1a55a6a1636318f30c155d8.webp)

## 使用锁的正确方式

**使用一把锁保护多个资源，只需要锁能覆盖所有受保护的资源就可以了。**

在上面的例子中， this 是对象级别的锁，所以 A 对象和 B 对象都有自己的锁。

让 A 对象和 B 对象共享一把锁的方式有很多。





```java title='让所有对象都持有一个唯一对象，这个对象在创建 Account 时传入。'

class Account {
  private Object lock；
  private int balance;
  private Account();
  // 创建Account时传入同一个lock对象
  public Account(Object lock) {
    this.lock = lock;
  }
  // 转账
  void transfer(Account target, int amt){
    // 此处检查所有对象共享的锁
    synchronized(lock) {
      if (this.balance > amt) {
        this.balance -= amt;
        target.balance += amt;
      }
    }
  }
}
```


```java title=' 用 Account.class 作为共享的锁。这个对象时在 Java 虚拟机加载 Account 类时创建的。所以一定是唯一的。'
class Account {
  private int balance;
  // 转账
  void transfer(Account target, int amt){
    synchronized(Account.class) {
      if (this.balance > amt) {
        this.balance -= amt;
        target.balance += amt;
      }
    }
  }
}
```

![527cd65f747abac3f23390663748da7c (1)](https://img.wkq.pub/pic/527cd65f747abac3f23390663748da7c%20(1).webp)

## 总结

**加锁能保证临界区代码的互斥性**，但还需要分析锁定的对象和受保护资源之间的关系，综合考虑受保护资源的访问路径，多方面考虑才能用好互斥锁。

锁，一定要有一个锁定的对象，至于这个锁定的对象要保护的资源以及在哪里加锁 /解锁，就属于设计层面的事情了。

如何保护多个资源，关键是要分析多个资源之间的关系。如果资源之间没有关系，很好处理，每个资源一把锁就可以了。如果资源之间有关联关系，就要选择一个粒度更大的锁，这个锁应该能够覆盖所有的相关资源。

除此之外，还要梳理出有那些访问路径，所有的访问路径都要设置合适的锁。

多个资源之间的关联关系其实是一种原子性特征，原子性的本质其实是不可分割，本质是多个资源有一致性的要求。操作的中间状态对不可见。

要解决原子性问题，是要保证中间状态对外不可见。
